/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 * 
 * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
 * 
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common
 * Development and Distribution License("CDDL") (collectively, the
 * "License"). You may not use this file except in compliance with the
 * License. You can obtain a copy of the License at
 * http://www.netbeans.org/cddl-gplv2.html
 * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
 * specific language governing permissions and limitations under the
 * License.  When distributing the software, include this License Header
 * Notice in each file and include the License file at
 * nbbuild/licenses/CDDL-GPL-2-CP.  Sun designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Sun in the GPL Version 2 section of the License file that
 * accompanied this code. If applicable, add the following below the
 * License Header, with the fields enclosed by brackets [] replaced by
 * your own identifying information:
 * "Portions Copyrighted [year] [name of copyright owner]"
 * 
 * Contributor(s):
 * 
 * The Original Software is NetBeans. The Initial Developer of the Original
 * Software is Sun Microsystems, Inc. Portions Copyright 1997-2007 Sun
 * Microsystems, Inc. All Rights Reserved.
 * 
 * If you wish your version of this file to be governed by only the CDDL
 * or only the GPL Version 2, indicate your decision by adding
 * "[Contributor] elects to include this software in this distribution
 * under the [CDDL or GPL Version 2] license." If you do not indicate a
 * single choice of license, a recipient has the option to distribute
 * your version of this file under either the CDDL, the GPL Version 2 or
 * to extend the choice of license to its licensees as provided above.
 * However, if you add GPL Version 2 code and therefore, elected the GPL
 * Version 2 license, then the option applies only if the new code is
 * made subject to such option by the copyright holder.
 */

package com.fluxtah.jordl.generator;

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * <p>
 * API for performing inflections (pluralization, singularization, and so on) on
 * various strings. These inflections will be useful in code generators that
 * convert things like database table names into Java class names.
 * </p>
 * 
 * <p>
 * The <code>getInstance()</code> method returns a singleton instance of this
 * class with a default set of rules, which can then be customized. Rules added
 * during customization will take precedence over the standard ones. Use the
 * <code>addIrregular()</code>, <code>addPlural()</code>,
 * <code>addSingular()</code>, and <code>addUncountable()</code> methods to add
 * additional rules ot the default ones.
 * </p>
 * 
 * <p>
 * <strong>IMPLEMENTATION NOTE</strong> - The default implementation is intended
 * to be functionally compatible with the <code>Inflector::inflections</code>
 * class in Ruby on Rails. The <code>gsub()</code> method on Ruby strings
 * matches regular expressions anywhere in the input. However, nearly all of the
 * actual patterns used in this module use <code>$</code> at the end to match
 * the end of the input string (so that only the last word in a multiple word
 * phrase will be singularized or pluralized). Therefore, the Java versions of
 * the regular expressions have been modified to capture all text before the
 * interesting characters at the end, and emit them as part of the result, so
 * that the entire string can be matched against a pattern once.
 * </p>
 */
public class Inflector {

	// ------------------------------------------------------------ Constructors

	/**
	 * <p>
	 * Private constructor to avoid instantiation.
	 * </p>
	 */
	private Inflector() {

		addPlural("$", "s", false);
		addPlural("(.*)$", "\\1s");
		addPlural("(.*)(ax|test)is$", "\\1\\2es");
		addPlural("(.*)(octop|vir)us$", "\\1\\2i");
		addPlural("(.*)(alias|status)$", "\\1\\2es");
		addPlural("(.*)(bu)s$", "\\1\\2ses");
		addPlural("(.*)(buffal|tomat)o$", "\\1\\2oes");
		addPlural("(.*)([ti])um$", "\\1\\2a");
		addPlural("(.*)sis$", "\\1ses");
		addPlural("(.*)(?:([^f])fe|([lr])f)$", "\\1\\3ves");
		addPlural("(.*)(hive)$", "\\1\\2s");
		addPlural("(.*)(tive)$", "\\1\\2s"); // Added for consistency with
												// singular rules
		addPlural("(.*)([^aeiouy]|qu)y$", "\\1\\2ies");
		addPlural("(.*)(series)$", "\\1\\2"); // Added for consistency with
												// singular rules
		addPlural("(.*)(movie)$", "\\1\\2s"); // Added for consistency with
												// singular rules
		addPlural("(.*)(x|ch|ss|sh)$", "\\1\\2es");
		addPlural("(.*)(matr|vert|ind)ix|ex$", "\\1\\2ices");
		addPlural("(.*)(o)$", "\\1\\2es"); // Added for consistency with
											// singular rules
		addPlural("(.*)(shoe)$", "\\1\\2s"); // Added for consistency with
												// singular rules
		addPlural("(.*)([m|l])ouse$", "\\1\\2ice");
		addPlural("^(ox)$", "\\1en");
		addPlural("(.*)(vert|ind)ex$", "\\1\\2ices"); // Added for consistency
														// with singular rules
		addPlural("(.*)(matr)ix$", "\\1\\2ices"); // Added for consistency with
													// singular rules
		addPlural("(.*)(quiz)$", "\\1\\2zes");

		addSingular("(.*)s$", "\\1");
		addSingular("(.*)(n)ews$", "\\1\\2ews");
		addSingular("(.*)([ti])a$", "\\1\\2um");
		addSingular(
				"(.*)((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$",
				"\\1\\2sis");
		addSingular("(.*)(^analy)ses$", "\\1\\2sis");
		addSingular("(.*)([^f])ves$", "\\1\\2fe");
		addSingular("(.*)(hive)s$", "\\1\\2");
		addSingular("(.*)(tive)s$", "\\1\\2");
		addSingular("(.*)([lr])ves$", "\\1\\2f");
		addSingular("(.*)([^aeiouy]|qu)ies$", "\\1\\2y");
		addSingular("(.*)(s)eries$", "\\1\\2eries");
		addSingular("(.*)(m)ovies$", "\\1\\2ovie");
		addSingular("(.*)(x|ch|ss|sh)es$", "\\1\\2");
		addSingular("(.*)([m|l])ice$", "\\1\\2ouse");
		addSingular("(.*)(bus)es$", "\\1\\2");
		addSingular("(.*)(o)es$", "\\1\\2");
		addSingular("(.*)(shoe)s$", "\\1\\2");
		addSingular("(.*)(cris|ax|test)es$", "\\1\\2is");
		addSingular("(.*)(octop|vir)i$", "\\1\\2us");
		addSingular("(.*)(alias|status)es$", "\\1\\2");
		addSingular("^(ox)en", "\\1");
		addSingular("(.*)(vert|ind)ices$", "\\1\\2ex");
		addSingular("(.*)(matr)ices$", "\\1\\2ix");
		addSingular("(.*)(quiz)zes$", "\\1\\2");

		addIrregular("child", "children");
		addIrregular("man", "men");
		addIrregular("move", "moves");
		addIrregular("person", "people");
		addIrregular("sex", "sexes");

		addUncountable("equipment");
		addUncountable("fish");
		addUncountable("information");
		addUncountable("money");
		addUncountable("rice");
		addUncountable("series");
		addUncountable("sheep");
		addUncountable("species");

	}

	// -------------------------------------------------------- Static Variables

	/**
	 * <p>
	 * The singleton instance returned by the default <code>getInstance()</code>
	 * method.
	 * </p>
	 */
	private transient static Inflector instance = null;

	/**
	 * <p>
	 * List of <code>Replacer</code>s for performing replacement operations on
	 * matches for plural words.
	 * </p>
	 */
	private List plurals = new LinkedList();

	/**
	 * <p>
	 * List of <code>Replacer</code>s for performing replacement operations on
	 * matches for addSingular words.
	 * </p>
	 */
	private List singulars = new ArrayList();

	/**
	 * <p>
	 * List of words that represent addUncountable concepts that cannot be
	 * pluralized or singularized.
	 * </p>
	 */
	private List uncountables = new LinkedList();

	// ------------------------------------------------------ Instance Variables

	// ---------------------------------------------------------- Static Methods

	/**
	 * <p>
	 * Return a fully configured {@link Inflector} instance that can be used for
	 * performing transformations.
	 * </p>
	 */
	public static Inflector getInstance() {

		if (instance == null) {
			instance = new Inflector();
		}
		return instance;

	}

	// ---------------------------------------------------------- Public Methods

	/**
	 * <p>
	 * Convert strings to <code>EmbeddedCamelCase</code>. Embedded underscores
	 * will be removed.
	 * </p>
	 * 
	 * @param word
	 *            Word to be converted
	 */
	public String camelize(String word) {

		return camelize(word, false);

	}

	/**
	 * <p>
	 * Convert word strings consisting of lower case letters and underscore
	 * characters between words into <code>embeddedCamelCase</code> or
	 * <code>EmbeddedCamelCase</code>, depending on the <code>lower</code> flag.
	 * Embedded underscores will be removed. Embedded '/' characters will be
	 * replaced by '.', making this method useful in converting path-like names
	 * into fully qualified classnames.
	 * </p>
	 * 
	 * <p>
	 * <strong>IMPLEMENTATION DIFFERENCE</strong> - The Rails version of this
	 * method also converts '/' characters to '::' because that reflects the
	 * normal syntax for fully qualified names in Ruby.
	 * </p>
	 * 
	 * <table border="1" width="100%">
	 * <tr>
	 * <th>Input</th>
	 * <th>Output</th>
	 * </tr>
	 * <tr>
	 * <td>"foo_bar", false</td>
	 * <td>"FooBar"</td>
	 * </tr>
	 * <tr>
	 * <td>"foo_bar", true</td>
	 * <td>"fooBar"</td>
	 * </tr>
	 * <tr>
	 * <td>"foo_bar/baz", false</td>
	 * <td>"FooBar.Baz"</td>
	 * </tr>
	 * <tr>
	 * <td>"foo_bar/baz", true</td>
	 * <td>"fooBar.Baz"</td>
	 * </tr>
	 * </table>
	 * 
	 * @param word
	 *            Word to be converted
	 * @param flag
	 *            Flag indicating that the initial character should be lower
	 *            cased instead of upper cased
	 */
	public String camelize(String word, boolean flag) {
		if (word.length() == 0)
			return word;

		StringBuffer sb = new StringBuffer(word.length());
		if (flag) {
			sb.append(Character.toLowerCase(word.charAt(0)));
		} else {
			sb.append(Character.toUpperCase(word.charAt(0)));
		}
		boolean capitalize = false;
		for (int i = 1; i < word.length(); i++) {
			char ch = word.charAt(i);
			if (capitalize) {
				sb.append(Character.toUpperCase(ch));
				capitalize = false;
			} else if (ch == '_') {
				capitalize = true;
			} else if (ch == '/') {
				capitalize = true;
				sb.append('.');
			} else {
				sb.append(ch);
			}
		}
		return sb.toString();

	}

	/**
	 * <p>
	 * Create and return a simple class name that corresponds to a addPlural
	 * table name. Any leading schema name will be trimmed.
	 * </p>
	 * 
	 * <table border="1" width="100%">
	 * <tr>
	 * <th>Input</th>
	 * <th>Output</th>
	 * </tr>
	 * <tr>
	 * <td>"foo_bars"</td>
	 * <td>"FooBar"</td>
	 * </tr>
	 * <tr>
	 * <td>"baz"</td>
	 * <td>"Baz"</td>
	 * </tr>
	 * </table>
	 * 
	 * @param tableName
	 *            Table name to be converted
	 */
	public String classify(String tableName) {

		int period = tableName.lastIndexOf('.');
		if (period >= 0) {
			tableName = tableName.substring(period + 1);
		}
		return camelize(singularize(tableName));

	}

	/**
	 * <p>
	 * Replace underscores in the specified word with dashes.
	 * </p>
	 * 
	 * <table border="1" width="100%">
	 * <tr>
	 * <th>Input</th>
	 * <th>Output</th>
	 * </tr>
	 * <tr>
	 * <td>"foo_bar"</td>
	 * <td>"foo-bar"</td>
	 * </tr>
	 * <tr>
	 * <td>"baz"</td>
	 * <td>"baz"</td>
	 * </tr>
	 * </table>
	 * 
	 * @param word
	 *            Word to be converted
	 */
	public String dasherize(String word) {

		return word.replace('_', '-');

	}

	/**
	 * <p>
	 * Remove any package name from a fully qualified class name, returning only
	 * the simple classname.
	 * </p>
	 * 
	 * <table border="1" width="100%">
	 * <tr>
	 * <th>Input</th>
	 * <th>Output</th>
	 * </tr>
	 * <tr>
	 * <td>"java.util.Map"</td>
	 * <td>"Map"</td>
	 * </tr>
	 * <tr>
	 * <td>"String"</td>
	 * <td>"String"</td>
	 * </tr>
	 * </table>
	 * 
	 * @param className
	 *            Fully qualified class name to be converted
	 */
	public String demodulize(String className) {

		int period = className.lastIndexOf('.');
		if (period >= 0) {
			return className.substring(period + 1);
		} else {
			return className;
		}

	}

	/**
	 * <p>
	 * Create and return a foreign key name from a class name, separating the
	 * "id" suffix with an underscore.
	 * </p>
	 */
	public String foreignKey(String className) {

		return foreignKey(className, true);

	}

	/**
	 * <p>
	 * Create and return a foreign key name from a class name, optionally
	 * inserting an underscore before the "id" portion.
	 * </p>
	 * 
	 * <table border="1" width="100%">
	 * <tr>
	 * <th>Input</th>
	 * <th>Output</th>
	 * </tr>
	 * <tr>
	 * <td>"com.mymodel.Order", false</td>
	 * <td>"orderid"</td>
	 * </tr>
	 * <tr>
	 * <td>"com.mymodel.Order", true</td>
	 * <td>"order_id"</td>
	 * </tr>
	 * <tr>
	 * <td>"Message", false</td>
	 * <td>"messageid"</td>
	 * </tr>
	 * <tr>
	 * <td>"Message", true</td>
	 * <td>"message_id"</td>
	 * </tr>
	 * </table>
	 * 
	 * @param className
	 *            Class name for which to create a foreign key
	 * @param underscore
	 *            Flag indicating whether an underscore should be emitted
	 *            between the class name and the "id" suffix
	 */
	public String foreignKey(String className, boolean underscore) {

		return underscore(demodulize(className) + (underscore ? "_id" : "id"));

	}

	/**
	 * <p>
	 * Capitalize the first word in a lower cased and underscored string, turn
	 * underscores into spaces, and string any trailing "_id". Like
	 * <code>titleize()</code>, this is meant for creating pretty output, and is
	 * not intended for code generation.
	 * </p>
	 * 
	 * <table border="1" width="100%">
	 * <tr>
	 * <th>Input</th>
	 * <th>Output</th>
	 * </tr>
	 * <tr>
	 * <td>"employee_salary"</td>
	 * <td>"Employee salary"</td>
	 * </tr>
	 * <tr>
	 * <td>"author_id"</td>
	 * <td>"Author"</td>
	 * </tr>
	 * </table>
	 * 
	 * @param words
	 *            Word string to be converted
	 */
	public String humanize(String words) {

		if (words.endsWith("_id")) {
			words = words.substring(0, words.length() - 3);
		}
		StringBuffer sb = new StringBuffer(words.length());
		sb.append(Character.toUpperCase(words.charAt(0)));
		for (int i = 1; i < words.length(); i++) {
			char ch = words.charAt(i);
			if (ch == '_') {
				sb.append(' ');
			} else {
				sb.append(ch);
			}
		}
		return sb.toString();

	}

	/**
	 * <p>
	 * Turn a number into a corresponding ordinal string used to denote the
	 * position in an ordered sequence.
	 * </p>
	 * 
	 * <table border="1" width="100%">
	 * <tr>
	 * <th>Input</th>
	 * <th>Output</th>
	 * </tr>
	 * <tr>
	 * <td>1</td>
	 * <td>"1st"</td>
	 * </tr>
	 * <tr>
	 * <td>2</td>
	 * <td>"2nd"</td>
	 * </tr>
	 * <tr>
	 * <td>3</td>
	 * <td>"3rd"</td>
	 * </tr>
	 * <tr>
	 * <td>4</td>
	 * <td>"rth"</td>
	 * </tr>
	 * <tr>
	 * <td>1002</td>
	 * <td>"1002nd"</td>
	 * </tr>
	 * <tr>
	 * <td>2012</td>
	 * <td>"2012th"</td>
	 * </tr>
	 * </table>
	 * 
	 * @param number
	 *            Number to be converted
	 */
	public String ordinalize(int number) {

		int modulo = number % 100;
		if ((modulo >= 11) && (modulo <= 13)) {
			return "" + number + "th";
		}
		switch (number % 10) {
		case 1:
			return "" + number + "st";
		case 2:
			return "" + number + "nd";
		case 3:
			return "" + number + "rd";
		default:
			return "" + number + "th";
		}

	}

	/**
	 * <p>
	 * Return a addPlural version of the specified (addSingular) word.
	 * </p>
	 * 
	 * 
	 * @param word
	 *            Singular word to be converted
	 */
	public String pluralize(String word) {

		// Scan uncountables and leave alone
		for (int i = 0; i < uncountables.size(); i++) {
			if (uncountables.get(i).equals(word)) {
				return word;
			}
		}

		// Scan our patterns for a match and return the correct replacement
		for (int i = 0; i < plurals.size(); i++) {
			Replacer replacer = (Replacer) plurals.get(i);
			if (replacer.matches(word)) {
				return replacer.replacement();
			}
		}

		// Return the original string unchanged
		return word;

	}

	/**
	 * <p>
	 * Return a addSingular version of the specified (addPlural) word.
	 * </p>
	 * 
	 * 
	 * @param word
	 *            Plural word to be converted
	 */
	public String singularize(String word) {

		// Scan uncountables and leave alone
		for (int i = 0; i < uncountables.size(); i++) {
			if (uncountables.get(i).equals(word)) {
				return word;
			}
		}

		// Scan our patterns for a match and return the correct replacement
		for (int i = 0; i < singulars.size(); i++) {
			Replacer replacer = (Replacer) singulars.get(i);
			if (replacer.matches(word)) {
				return replacer.replacement();
			}
		}

		// Return the original string unchanged
		return word;

	}

	/**
	 * <p>
	 * Convert the simple name of a model class into the corresponding name of a
	 * database table, by uncamelizing, inserting underscores, and pluralizing
	 * the last word.
	 * </p>
	 * 
	 * <table border="1" width="100%">
	 * <tr>
	 * <th>Input</th>
	 * <th>Output</th>
	 * </tr>
	 * <tr>
	 * <td>"RawScaledScorer"</td>
	 * <td>"raw_scaled_scorers"</td>
	 * </tr>
	 * <tr>
	 * <td>"fancyCategory"</td>
	 * <td>"fancy_categories"</td>
	 * </tr>
	 * </table>
	 * 
	 * @param className
	 *            Class name to be converted
	 */
	public String tableize(String className) {

		return pluralize(underscore(className));

	}

	/**
	 * <p>
	 * Capitalize all the words, and replace some characters in the string to
	 * create a nicer looking title. This is meant for creating pretty output,
	 * and is not intended for code generation.
	 * </p>
	 * 
	 * <table border="1" width="100%">
	 * <tr>
	 * <th>Input</th>
	 * <th>Output</th>
	 * </tr>
	 * <tr>
	 * <td>"the honeymooners"</td>
	 * <td>"The Honeymooners"</td>
	 * </tr>
	 * <tr>
	 * <td>"x-men: the last stand"</td>
	 * <td>"X Men: The Last Stand"</td>
	 * </tr>
	 * </table>
	 * 
	 * @param words
	 *            Word string to be converted
	 */
	public String titleize(String words) {

		StringBuffer sb = new StringBuffer(words.length());
		boolean capitalize = true; // To get the first character right
		for (int i = 0; i < words.length(); i++) {
			char ch = words.charAt(i);
			if (Character.isWhitespace(ch)) {
				sb.append(' ');
				capitalize = true;
			} else if (ch == '-') {
				sb.append(' ');
				capitalize = true;
			} else if (capitalize) {
				sb.append(Character.toUpperCase(ch));
				capitalize = false;
			} else {
				sb.append(ch);
			}
		}
		return sb.toString();

	}

	/**
	 * <p>
	 * The reverse of <code>camelize()</code>, makes an underscored form from
	 * the expression in the string. Changes "." to "/" to convert fully
	 * qualified class names into paths.
	 * </p>
	 * 
	 * <table border="1" width="100%">
	 * <tr>
	 * <th>Input</th>
	 * <th>Output</th>
	 * </tr>
	 * <tr>
	 * <td>"FooBar"</td>
	 * <td>"foo_bar"</td>
	 * </tr>
	 * <tr>
	 * <td>"fooBar"</td>
	 * <td>"foo_bar"</td>
	 * </tr>
	 * <tr>
	 * <td>"FooBar.Baz"</td>
	 * <td>"foo_bar/baz"</td>
	 * </tr>
	 * <tr>
	 * <td>"FooBar.Baz"</td>
	 * <td>"foo_bar/baz"</td>
	 * </tr>
	 * </table>
	 * 
	 * @param word
	 *            Camel cased word to be converted
	 */
	public String underscore(String word) {

		StringBuffer sb = new StringBuffer(word.length() + 5);
		boolean uncapitalize = false;
		for (int i = 0; i < word.length(); i++) {
			char ch = word.charAt(i);
			if (uncapitalize) {
				sb.append(Character.toLowerCase(ch));
				uncapitalize = false;
			} else if (ch == '.') {
				sb.append('/');
				uncapitalize = true;
			} else if (Character.isUpperCase(ch)) {
				if (i > 0) {
					sb.append('_');
				}
				sb.append(Character.toLowerCase(ch));
			} else {
				sb.append(ch);
			}
		}
		return sb.toString();

	}

	// --------------------------------------------------- Customization Methods

	/**
	 * <p>
	 * Add the addSingular and addPlural forms of words that cannot be converted
	 * using the normal rules.
	 * </p>
	 * 
	 * 
	 * @param singular
	 *            Singular form of the word
	 * @param plural
	 *            Plural form of the word
	 */
	public void addIrregular(String singular, String plural) {

		addPlural(
				"(.*)(" + singular.substring(0, 1) + ")"
						+ singular.substring(1) + "$",
				"\\1\\2" + plural.substring(1));
		addSingular(
				"(.*)(" + plural.substring(0, 1) + ")" + plural.substring(1)
						+ "$", "\\1\\2" + singular.substring(1));

	}

	/**
	 * <p>
	 * Add a match pattern and replacement rule for converting addPlural forms
	 * to addSingular forms. By default, matches will be case insensitive.
	 * </p>
	 * 
	 * 
	 * @param match
	 *            Match pattern regular expression
	 * @param rule
	 *            Replacement rule
	 */
	public void addPlural(String match, String rule) {

		addPlural(match, rule, true);

	}

	/**
	 * <p>
	 * Add a match pattern and replacement rule for converting addPlural forms
	 * to addSingular forms.
	 * </p>
	 * 
	 * 
	 * @param match
	 *            Match pattern regular expression
	 * @param rule
	 *            Replacement rule
	 * @param insensitive
	 *            Flag indicating this match should be case insensitive
	 */
	public void addPlural(String match, String rule, boolean insensitive) {

		plurals.add(0, new Replacer(match, rule, insensitive));

	}

	/**
	 * <p>
	 * Add a match pattern and replacement rule for converting addSingular forms
	 * to addPlural forms. By default, matches will be case insensitive.
	 * </p>
	 * 
	 * 
	 * @param match
	 *            Match pattern regular expression
	 * @param rule
	 *            Replacement rule
	 */
	public void addSingular(String match, String rule) {

		addSingular(match, rule, true);

	}

	/**
	 * <p>
	 * Add a match pattern and replacement rule for converting addSingular forms
	 * to addPlural forms.
	 * </p>
	 * 
	 * 
	 * @param match
	 *            Match pattern regular expression
	 * @param rule
	 *            Replacement rule
	 * @param insensitive
	 *            Flag indicating this match should be case insensitive
	 */
	public void addSingular(String match, String rule, boolean insensitive) {

		singulars.add(0, new Replacer(match, rule, insensitive));

	}

	/**
	 * <p>
	 * Add a word that cannot be converted between addSingular and addPlural.
	 * </p>
	 * 
	 * 
	 * @param word
	 *            Word to be added
	 */
	public void addUncountable(String word) {

		uncountables.add(0, word.toLowerCase());

	}

	// --------------------------------------------------------- Private Classes

	/**
	 * <p>
	 * Internal class that uses a regular expression matcher to both match the
	 * specified regular expression to a specified word, and (if successful)
	 * perform the appropriate substitutions.
	 * </p>
	 */
	private class Replacer {

		// --------------------------------------------------------- Constructor

		public Replacer(String match, String rule, boolean insensitive) {

			pattern = Pattern.compile(match,
					insensitive ? Pattern.CASE_INSENSITIVE : 0);
			this.rule = rule;

		}

		// -------------------------------------------------- Instance Variables

		private String input = null;
		private Matcher matcher = null;
		private Pattern pattern = null;
		private String rule = null;

		// ------------------------------------------------------ Public Methods

		/**
		 * <p>
		 * Return <code>true</code> if our regular expression pattern matches
		 * the specified input. If it does, save necessary state information so
		 * that the <code>replacement()</code> method will return appropriate
		 * results based on the <code>rule</code> specified to our constructor.
		 * </p>
		 * 
		 * @param input
		 *            Input characters to be matched
		 */
		public boolean matches(String input) {

			matcher = pattern.matcher(input);
			if (matcher.matches()) {
				this.input = input;
				return true;
			} else {
				this.input = null;
				this.matcher = null;
				return false;
			}

		}

		/**
		 * <p>
		 * Return a replacement string based on the <code>rule</code> that was
		 * specified to our constructor. This method <strong>MUST</strong> only
		 * be called when the <code>matches()</code> method has returned
		 * <code>true</code>.
		 * </p>
		 */
		public String replacement() {

			StringBuffer sb = new StringBuffer();
			boolean group = false;
			for (int i = 0; i < rule.length(); i++) {
				char ch = rule.charAt(i);
				if (group) {
					sb.append(matcher.group(Character.digit(ch, 10)));
					group = false;
				} else if (ch == '\\') {
					group = true;
				} else {
					sb.append(ch);
				}
			}
			return sb.toString();

		}

	}

}
